package com.yanyeori.framework.core.util;

import com.yanyeori.framework.core.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

/**
 * File操作工具类
 *
 * @author chenkuan 2019/4/17
 */
@Slf4j
public class FileUtil extends FileCopyUtils {
    /**
     * 文件写入
     *
     * @param file      待写入字符串
     * @param filePath  文件存放路径
     * @param fileName  文件名
     * @param overwrite 是否覆盖（否则重命名）
     * @return File 返回最终写入的文件
     */
    public static File write(String file, String filePath, String fileName, boolean overwrite) {
        return write(file.getBytes(StandardCharsets.UTF_8), filePath, fileName, overwrite);
    }

    /**
     * 文件写入
     *
     * @param file      文件字节数组byte[]
     * @param filePath  文件存放路径
     * @param fileName  文件名
     * @param overwrite 是否覆盖（否则重命名）
     * @return File 返回最终写入的文件
     */
    public static File write(byte[] file, String filePath, String fileName, boolean overwrite) {
        //参数校验
        validUpload(filePath, fileName);
        //文件夹路径末尾标识符处理
        filePath = handleDirPathEnd(filePath);

        FileOutputStream out = null;
        try {
            // 判断目标文件夹是否存在，不存在则新建
            File targetFile = new File(filePath);
            if (!targetFile.exists()) {
                targetFile.mkdirs();
            } else {
                if (overwrite) {
                    // 判断目标文件是否存在，存在则删除
                    deleteIfExist(filePath, fileName);
                } else {
                    // 判断目标文件是否存在，存在则重命名
                    fileName = renameIfExist(filePath, fileName);
                }
            }
            out = new FileOutputStream(filePath + fileName);
            out.write(file);
            out.flush();
            // 返回最终写入的文件
            return new File(filePath + fileName);
        } catch (IOException e) {
            throw new ServiceException(e, "[{}{} write failed]", filePath, fileName);
        } finally {
            close(out);
        }
    }

    /**
     * 文件写入
     *
     * @param inputStream 文件流
     * @param filePath    文件存放路径
     * @param fileName    文件名
     * @param overwrite   是否覆盖（否则重命名）
     * @return File 返回最终写入的文件
     */
    public static File write(InputStream inputStream, String filePath, String fileName, boolean overwrite) {
        //参数校验
        validUpload(filePath, fileName);
        //文件夹路径末尾标识符处理
        filePath = handleDirPathEnd(filePath);

        BufferedInputStream bis = null;
        FileOutputStream out = null;
        try {
            // 判断目标文件夹是否存在，不存在则新建
            File targetFile = new File(filePath);
            if (!targetFile.exists()) {
                targetFile.mkdirs();
            } else {
                if (overwrite) {
                    // 判断目标文件是否存在，存在则删除
                    deleteIfExist(filePath, fileName);
                } else {
                    // 判断目标文件是否存在，存在则重命名
                    fileName = renameIfExist(filePath, fileName);
                }
            }
            byte[] buffer = new byte[StreamUtils.BUFFER_SIZE];
            bis = new BufferedInputStream(inputStream);
            out = new FileOutputStream(filePath + fileName);
            int read;
            while ((read = bis.read(buffer)) > -1) {
                out.write(buffer, 0, read);
            }
            out.flush();
            // 返回最终写入的文件
            return new File(filePath + fileName);
        } catch (IOException e) {
            throw new ServiceException(e, "[{}{} write failed]", filePath, fileName);
        } finally {
            close(out);
            close(bis);
        }
    }

    /**
     * 读取文件到客户端 支持迅雷、网际快车等客户端的断点续传及分片下载
     *
     * @param request  HttpServletRequest
     * @param response HttpServletResponse
     * @param file     下载的文件
     */
    public static void readToClient(HttpServletRequest request, HttpServletResponse response, File file) {
        // 文件总大小
        long fileLength = file.length();

        // 迅雷等工具
        // 用于记录客户端要求下载的数据范围字串(用于断点续传)
        String rangeBytes = request.getHeader("Range");
        // 客户端需要的第一个字节的位置
        long lenStart = 0;
        // 客户端需要的字节区间 最后一个字节位置 - 第一个字节的位置 + 1
        long lenEnd = fileLength;
        if (StringUtil.isNotEmpty(rangeBytes)) {
            // 返回码 200 Ok (不使用断点续传方式), 206 Partial Content (使用断点续传方式)
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            rangeBytes = rangeBytes.replaceAll("bytes=", "");
            String startBytes;
            String endBytes;
            if (rangeBytes.indexOf('-') == rangeBytes.length() - 1) {
                // 无结束字符 网际快车等
                startBytes = rangeBytes.substring(0, rangeBytes.indexOf('-'));
                lenStart = Long.parseLong(startBytes.trim());
                lenEnd = fileLength - lenStart;
            } else {
                // 迅雷等
                startBytes = rangeBytes.substring(0, rangeBytes.indexOf('-')).trim();
                endBytes = rangeBytes.substring(rangeBytes.indexOf('-') + 1).trim();
                lenStart = Long.parseLong(startBytes);
                lenEnd = Long.parseLong(endBytes) - lenStart + 1; //字节段包含开始字节和结束字节，需+1得到真实字节段长度
            }
            // 通知客户端允许断点续传，响应格式为：Accept-Ranges: bytes
            response.setHeader("Accept-Ranges", "bytes");
        }
        if (lenStart != 0) {
            // 断点续传
            // 服务器返回当前接受的范围和文件总大小
            // 响应格式 Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity legth]
            String contentRange = rangeBytes + "/" + fileLength;
            response.setHeader("Content-Range", contentRange);
        }

        // 文件名
        String fileName = file.getName();
        // 告诉浏览器输出内容格式 application/octet-stream，application/force-download
        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
        // 设置下载而并不是打开 两种属性：attachment保存，inline直接显示  浏览器点保存后，文件以filename的值命名
        response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtil.encodeUrl(fileName));
        // 设置文件大小
        response.setContentLengthLong(fileLength);

        long hasReadBytes = 0L; // 已总共读取字节数
        long notReadBytes = lenEnd - hasReadBytes; // 客户端要求还未读取字节数
        int initTem = 1024 * 8; // 默认一次缓存字节读取数
        byte[] buffer;
        if (notReadBytes < initTem) {
            buffer = new byte[(int) notReadBytes];
        } else {
            buffer = new byte[initTem];
        }
        RandomAccessFile raf = null;
        try {
            raf = new RandomAccessFile(file, "r");
            // 从指定位置开始读取
            if (lenStart != 0) raf.seek(lenStart);
            OutputStream os = response.getOutputStream();
            int num;
            while ((num = raf.read(buffer)) != -1 && notReadBytes > 0) {
                os.write(buffer, 0, num);
                hasReadBytes += num;
                notReadBytes = lenEnd - hasReadBytes;
                if (notReadBytes < initTem) buffer = new byte[(int) notReadBytes];
            }
            log.debug((StringUtil.isEmpty(rangeBytes) ? fileName : fileName + " (" + rangeBytes + ") ") + "下载完成");
        } catch (ClientAbortException e) {
            // log.error("客户端中断了连接");
        } catch (IOException e) {
            throw new ServiceException(e, "[{}] download failed", fileName);
        } finally {
            close(raf);
        }
    }

    /**
     * 判断目标文件是否存在，存在则删除
     *
     * @param filePath 文件路径
     * @param fileName 文件名
     */
    public static void deleteIfExist(String filePath, String fileName) {
        File file = new File(handleDirPathEnd(filePath) + fileName);
        if (file.exists()) {
            file.delete();
        }
    }

    /**
     * 判断目标文件是否存在，存在则重命名 file file (1) file (2)
     *
     * @param filePath 文件路径
     * @param fileName 文件名
     * @return String 文件名
     */
    public static String renameIfExist(String filePath, String fileName) {
        File file = new File(filePath + fileName);
        String fileNameTemp = fileName;
        int num = 1;
        while (file.exists()) {
            fileNameTemp = fileName;
            int lastPointIndex = fileNameTemp.lastIndexOf(".");
            if (lastPointIndex != -1) {
                fileNameTemp = fileNameTemp.substring(0, lastPointIndex) + " (" + num + ")" + fileNameTemp.substring(lastPointIndex);
            } else {
                fileNameTemp += " (" + num + ")";
            }
            file = new File(filePath + fileNameTemp);
            num++;
        }
        return fileNameTemp;
    }

    private static void validUpload(String filePath, String fileName) {
        Objects.requireNonNull(filePath, "The file path cannot be empty");
        Objects.requireNonNull(fileName, "The file name cannot be empty");
    }

    public static String handleDirPathEnd(String dirPath) {
        if (!(dirPath.endsWith("/") || dirPath.endsWith("\\"))) {
            dirPath += "/";
        }
        return dirPath;
    }

    public static void close(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (Exception e) {
                // ignore
            }
        }
    }

    /**
     * 获取项目根目录，定义文件夹存放路径
     */
    public static String getUserDir() {
        return System.getProperty("user.dir") + "/";
    }

    /**
     * 设置下载文件头
     */
    public static void setDownloadHeader(HttpServletResponse response, String filename, @Nullable Long fileLength) {
        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
        if (fileLength != null) {
            response.setContentLengthLong(fileLength);
        }
        filename = ServletUtil.isSafariBrowser() ? new String(filename.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1)
                : HttpUtil.encodeUrl(filename);
        response.setHeader("Content-Disposition", "attachment;filename=" + filename);
        response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
        response.setCharacterEncoding("utf-8");
    }
}
